Clean up video IO
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m33s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-22 15:16:35 +10:00
parent 7bf5464fd2
commit 9787ca5f27
15 changed files with 377 additions and 255 deletions

View File

@@ -106,6 +106,14 @@ set(SHADER_MANIFEST_SOURCES
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
)
set(VIDEO_MODE_SOURCES
"${SRC_DIR}/video/core/VideoMode.cpp"
)
set(VIDEO_FORMAT_SOURCES
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
)
set(SLANG_RUNTIME_FILES
"${SLANG_ROOT}/bin/slangc.exe"
"${SLANG_ROOT}/bin/slang-compiler.dll"

View File

@@ -23,7 +23,7 @@ Native app internals are grouped by boundary:
- `render/`: render thread, readback, runtime render scene, and shared-context shader program preparation.
- `runtime/`: shader catalog support, layer model, Slang build bridge, font atlas build, and runtime-state persistence.
- `shader/`: shader package parsing and Slang compilation helpers.
- `video/core/`: backend-neutral video IO handoff contracts, formats, and output scheduling thread.
- `video/core/`: backend-neutral video IO handoff contracts, mode descriptions, pixel formats, and output scheduling thread.
- `video/decklink/`: current DeckLink input/output backend.
- `video/playout/`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers.
- `video/legacy/`: older backend pipeline pieces kept separate while the new edge model settles.
@@ -164,7 +164,7 @@ Current native test coverage includes:
}
```
`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes 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`.
`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.

View File

@@ -25,7 +25,7 @@ Primary source areas:
- `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox
- `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker
- `src/frames`: system-memory frame exchange
- `src/video/core`: generic video IO edge contracts, formats, and output scheduling thread
- `src/video/core`: generic video IO edge contracts, mode descriptions, formats, and output scheduling thread
- `src/video/decklink`: current DeckLink input/output backend
- `src/video/playout`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers
- `src/video/legacy`: older backend pipeline pieces kept separate from the current app path
@@ -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.
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`.
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.

View File

@@ -7,8 +7,9 @@
#include "render/thread/RenderThread.h"
#include "video/decklink/DeckLinkInput.h"
#include "video/decklink/DeckLinkInputThread.h"
#include "DeckLinkDisplayMode.h"
#include "VideoIOFormat.h"
#include "video/decklink/DeckLinkDisplayMode.h"
#include "video/core/VideoIOFormat.h"
#include "video/core/VideoMode.h"
#include <windows.h>
@@ -93,7 +94,7 @@ int main(int argc, char** argv)
}
SystemFrameExchangeConfig frameExchangeConfig;
RenderCadenceCompositor::VideoFormatDimensions(
VideoFormatDimensions(
appConfig.outputVideoFormat,
frameExchangeConfig.width,
frameExchangeConfig.height);
@@ -108,7 +109,7 @@ int main(int argc, char** argv)
SystemFrameExchange frameExchange(frameExchangeConfig);
InputFrameMailboxConfig inputMailboxConfig;
RenderCadenceCompositor::VideoFormatDimensions(
VideoFormatDimensions(
appConfig.inputVideoFormat,
inputMailboxConfig.width,
inputMailboxConfig.height);
@@ -194,9 +195,9 @@ int main(int argc, char** argv)
RenderThread::Config renderConfig;
renderConfig.width = frameExchangeConfig.width;
renderConfig.height = frameExchangeConfig.height;
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
const double fallbackFrameDurationMilliseconds = FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
? outputVideoMode.frameDurationMilliseconds
: fallbackFrameDurationMilliseconds;
renderConfig.pboDepth = kReadbackDepth;

View File

@@ -2,8 +2,6 @@
#include "RuntimeJson.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <fstream>
@@ -166,92 +164,6 @@ void AppConfigProvider::ApplyCommandLine(int argc, char** argv)
}
}
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate)
{
double rate = fallbackRate;
try
{
rate = std::stod(rateText);
}
catch (...)
{
rate = fallbackRate;
}
if (rate <= 0.0)
rate = fallbackRate;
return 1000.0 / rate;
}
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
{
struct ModeRate
{
BMDDisplayMode mode;
int64_t frameDuration;
int64_t timeScale;
};
static const ModeRate rates[] =
{
{ bmdModeHD720p50, 1, 50 },
{ bmdModeHD720p5994, 1001, 60000 },
{ bmdModeHD720p60, 1, 60 },
{ bmdModeHD1080i50, 1, 25 },
{ bmdModeHD1080i5994, 1001, 30000 },
{ bmdModeHD1080i6000, 1, 30 },
{ bmdModeHD1080p2398, 1001, 24000 },
{ bmdModeHD1080p24, 1, 24 },
{ bmdModeHD1080p25, 1, 25 },
{ bmdModeHD1080p2997, 1001, 30000 },
{ bmdModeHD1080p30, 1, 30 },
{ bmdModeHD1080p50, 1, 50 },
{ bmdModeHD1080p5994, 1001, 60000 },
{ bmdModeHD1080p6000, 1, 60 },
{ bmdMode4K2160p2398, 1001, 24000 },
{ bmdMode4K2160p24, 1, 24 },
{ bmdMode4K2160p25, 1, 25 },
{ bmdMode4K2160p2997, 1001, 30000 },
{ bmdMode4K2160p30, 1, 30 },
{ bmdMode4K2160p50, 1, 50 },
{ bmdMode4K2160p5994, 1001, 60000 },
{ bmdMode4K2160p60, 1, 60 }
};
for (const ModeRate& rate : rates)
{
if (rate.mode == displayMode && rate.timeScale > 0)
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
}
return fallbackMilliseconds;
}
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
{
std::string normalized = formatName;
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
if (normalized == "720p")
{
width = 1280;
height = 720;
return;
}
if (normalized == "2160p" || normalized == "4k" || normalized == "uhd")
{
width = 3840;
height = 2160;
return;
}
width = 1920;
height = 1080;
}
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
{
return FindRepoPath(relativePath);

View File

@@ -1,7 +1,7 @@
#pragma once
#include "AppConfig.h"
#include "DeckLinkDisplayMode.h"
#include "VideoMode.h"
#include <filesystem>
#include <string>
@@ -27,9 +27,6 @@ private:
bool mLoadedFromFile = false;
};
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
}

View File

@@ -1,7 +1,7 @@
#pragma once
#include "DeckLinkDisplayMode.h"
#include "VideoIOFormat.h"
#include "VideoMode.h"
#include <cstdint>
#include <functional>

View File

@@ -0,0 +1,160 @@
#include "VideoMode.h"
#include <algorithm>
#include <cctype>
std::string NormalizeModeToken(const std::string& value)
{
std::string normalized;
for (unsigned char ch : value)
{
if (std::isalnum(ch))
normalized.push_back(static_cast<char>(std::tolower(ch)));
}
return normalized;
}
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate)
{
double rate = fallbackRate;
try
{
rate = std::stod(rateText);
}
catch (...)
{
rate = fallbackRate;
}
if (rate <= 0.0)
rate = fallbackRate;
return 1000.0 / rate;
}
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
{
std::string normalized = formatName;
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
if (normalized == "720p")
{
width = 1280;
height = 720;
return;
}
if (normalized == "2160p" || normalized == "4k" || normalized == "uhd")
{
width = 3840;
height = 2160;
return;
}
width = 1920;
height = 1080;
}
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode)
{
const std::string formatToken = NormalizeModeToken(videoFormat);
const std::string frameToken = NormalizeModeToken(frameRate);
const std::string combinedToken = formatToken + frameToken;
struct ModeOption
{
const char* token;
const char* formatName;
const char* frameRate;
const char* displayName;
};
static const ModeOption options[] =
{
{ "720p50", "720p", "50", "720p50" },
{ "hd720p50", "720p", "50", "720p50" },
{ "720p5994", "720p", "59.94", "720p59.94" },
{ "hd720p5994", "720p", "59.94", "720p59.94" },
{ "720p60", "720p", "60", "720p60" },
{ "hd720p60", "720p", "60", "720p60" },
{ "1080i50", "1080i", "50", "1080i50" },
{ "hd1080i50", "1080i", "50", "1080i50" },
{ "1080i5994", "1080i", "59.94", "1080i59.94" },
{ "hd1080i5994", "1080i", "59.94", "1080i59.94" },
{ "1080i60", "1080i", "60", "1080i60" },
{ "hd1080i60", "1080i", "60", "1080i60" },
{ "1080p2398", "1080p", "23.98", "1080p23.98" },
{ "hd1080p2398", "1080p", "23.98", "1080p23.98" },
{ "1080p24", "1080p", "24", "1080p24" },
{ "hd1080p24", "1080p", "24", "1080p24" },
{ "1080p25", "1080p", "25", "1080p25" },
{ "hd1080p25", "1080p", "25", "1080p25" },
{ "1080p2997", "1080p", "29.97", "1080p29.97" },
{ "hd1080p2997", "1080p", "29.97", "1080p29.97" },
{ "1080p30", "1080p", "30", "1080p30" },
{ "hd1080p30", "1080p", "30", "1080p30" },
{ "1080p50", "1080p", "50", "1080p50" },
{ "hd1080p50", "1080p", "50", "1080p50" },
{ "1080p5994", "1080p", "59.94", "1080p59.94" },
{ "hd1080p5994", "1080p", "59.94", "1080p59.94" },
{ "1080p60", "1080p", "60", "1080p60" },
{ "hd1080p60", "1080p", "60", "1080p60" },
{ "2160p2398", "2160p", "23.98", "2160p23.98" },
{ "4k2160p2398", "2160p", "23.98", "2160p23.98" },
{ "2160p24", "2160p", "24", "2160p24" },
{ "4k2160p24", "2160p", "24", "2160p24" },
{ "2160p25", "2160p", "25", "2160p25" },
{ "4k2160p25", "2160p", "25", "2160p25" },
{ "2160p2997", "2160p", "29.97", "2160p29.97" },
{ "4k2160p2997", "2160p", "29.97", "2160p29.97" },
{ "2160p30", "2160p", "30", "2160p30" },
{ "4k2160p30", "2160p", "30", "2160p30" },
{ "2160p50", "2160p", "50", "2160p50" },
{ "4k2160p50", "2160p", "50", "2160p50" },
{ "2160p5994", "2160p", "59.94", "2160p59.94" },
{ "4k2160p5994", "2160p", "59.94", "2160p59.94" },
{ "2160p60", "2160p", "60", "2160p60" },
{ "4k2160p60", "2160p", "60", "2160p60" }
};
for (const ModeOption& option : options)
{
if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token))
{
videoMode.formatName = option.formatName;
videoMode.frameRate = option.frameRate;
videoMode.displayName = option.displayName;
VideoFormatDimensions(videoMode.formatName, videoMode.frameSize.width, videoMode.frameSize.height);
videoMode.frameDurationMilliseconds = FrameDurationMillisecondsFromRateString(videoMode.frameRate);
return true;
}
}
return false;
}
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error)
{
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
{
error = "Unsupported inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
inputVideoFormat + " / " + inputFrameRate;
return false;
}
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
{
error = "Unsupported outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
outputVideoFormat + " / " + outputFrameRate;
return false;
}
return true;
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include <string>
struct FrameSize
{
unsigned width = 0;
unsigned height = 0;
bool IsEmpty() const { return width == 0 || height == 0; }
};
inline bool operator==(const FrameSize& left, const FrameSize& right)
{
return left.width == right.width && left.height == right.height;
}
inline bool operator!=(const FrameSize& left, const FrameSize& right)
{
return !(left == right);
}
struct VideoFormat
{
std::string formatName = "1080p";
std::string frameRate = "59.94";
std::string displayName = "1080p59.94";
FrameSize frameSize = { 1920, 1080 };
double frameDurationMilliseconds = 1000.0 / 59.94;
};
struct VideoFormatSelection
{
VideoFormat input;
VideoFormat output;
};
std::string NormalizeModeToken(const std::string& value);
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
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& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error);

View File

@@ -1,16 +1,50 @@
#include "DeckLinkDisplayMode.h"
#include <cctype>
std::string NormalizeModeToken(const std::string& value)
bool DeckLinkDisplayModeForVideoFormat(const VideoFormat& videoMode, BMDDisplayMode& displayMode)
{
std::string normalized;
for (unsigned char ch : value)
struct ModeOption
{
if (std::isalnum(ch))
normalized.push_back(static_cast<char>(std::tolower(ch)));
const char* displayName;
BMDDisplayMode mode;
};
static const ModeOption options[] =
{
{ "720p50", bmdModeHD720p50 },
{ "720p59.94", bmdModeHD720p5994 },
{ "720p60", bmdModeHD720p60 },
{ "1080i50", bmdModeHD1080i50 },
{ "1080i59.94", bmdModeHD1080i5994 },
{ "1080i60", bmdModeHD1080i6000 },
{ "1080p23.98", bmdModeHD1080p2398 },
{ "1080p24", bmdModeHD1080p24 },
{ "1080p25", bmdModeHD1080p25 },
{ "1080p29.97", bmdModeHD1080p2997 },
{ "1080p30", bmdModeHD1080p30 },
{ "1080p50", bmdModeHD1080p50 },
{ "1080p59.94", bmdModeHD1080p5994 },
{ "1080p60", bmdModeHD1080p6000 },
{ "2160p23.98", bmdMode4K2160p2398 },
{ "2160p24", bmdMode4K2160p24 },
{ "2160p25", bmdMode4K2160p25 },
{ "2160p29.97", bmdMode4K2160p2997 },
{ "2160p30", bmdMode4K2160p30 },
{ "2160p50", bmdMode4K2160p50 },
{ "2160p59.94", bmdMode4K2160p5994 },
{ "2160p60", bmdMode4K2160p60 }
};
const std::string displayToken = NormalizeModeToken(videoMode.displayName);
for (const ModeOption& option : options)
{
if (displayToken == NormalizeModeToken(option.displayName))
{
displayMode = option.mode;
return true;
}
}
return normalized;
return false;
}
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName)
@@ -19,108 +53,55 @@ bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::str
if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode))
return false;
displayMode = videoMode.displayMode;
if (!DeckLinkDisplayModeForVideoFormat(videoMode, displayMode))
return false;
displayModeName = videoMode.displayName;
return true;
}
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode)
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
{
const std::string formatToken = NormalizeModeToken(videoFormat);
const std::string frameToken = NormalizeModeToken(frameRate);
const std::string combinedToken = formatToken + frameToken;
struct ModeOption
struct ModeRate
{
const char* token;
BMDDisplayMode mode;
const char* displayName;
int64_t frameDuration;
int64_t timeScale;
};
static const ModeOption options[] =
static const ModeRate rates[] =
{
{ "720p50", bmdModeHD720p50, "720p50" },
{ "hd720p50", bmdModeHD720p50, "720p50" },
{ "720p5994", bmdModeHD720p5994, "720p59.94" },
{ "hd720p5994", bmdModeHD720p5994, "720p59.94" },
{ "720p60", bmdModeHD720p60, "720p60" },
{ "hd720p60", bmdModeHD720p60, "720p60" },
{ "1080i50", bmdModeHD1080i50, "1080i50" },
{ "hd1080i50", bmdModeHD1080i50, "1080i50" },
{ "1080i5994", bmdModeHD1080i5994, "1080i59.94" },
{ "hd1080i5994", bmdModeHD1080i5994, "1080i59.94" },
{ "1080i60", bmdModeHD1080i6000, "1080i60" },
{ "hd1080i60", bmdModeHD1080i6000, "1080i60" },
{ "1080p2398", bmdModeHD1080p2398, "1080p23.98" },
{ "hd1080p2398", bmdModeHD1080p2398, "1080p23.98" },
{ "1080p24", bmdModeHD1080p24, "1080p24" },
{ "hd1080p24", bmdModeHD1080p24, "1080p24" },
{ "1080p25", bmdModeHD1080p25, "1080p25" },
{ "hd1080p25", bmdModeHD1080p25, "1080p25" },
{ "1080p2997", bmdModeHD1080p2997, "1080p29.97" },
{ "hd1080p2997", bmdModeHD1080p2997, "1080p29.97" },
{ "1080p30", bmdModeHD1080p30, "1080p30" },
{ "hd1080p30", bmdModeHD1080p30, "1080p30" },
{ "1080p50", bmdModeHD1080p50, "1080p50" },
{ "hd1080p50", bmdModeHD1080p50, "1080p50" },
{ "1080p5994", bmdModeHD1080p5994, "1080p59.94" },
{ "hd1080p5994", bmdModeHD1080p5994, "1080p59.94" },
{ "1080p60", bmdModeHD1080p6000, "1080p60" },
{ "hd1080p60", bmdModeHD1080p6000, "1080p60" },
{ "2160p2398", bmdMode4K2160p2398, "2160p23.98" },
{ "4k2160p2398", bmdMode4K2160p2398, "2160p23.98" },
{ "2160p24", bmdMode4K2160p24, "2160p24" },
{ "4k2160p24", bmdMode4K2160p24, "2160p24" },
{ "2160p25", bmdMode4K2160p25, "2160p25" },
{ "4k2160p25", bmdMode4K2160p25, "2160p25" },
{ "2160p2997", bmdMode4K2160p2997, "2160p29.97" },
{ "4k2160p2997", bmdMode4K2160p2997, "2160p29.97" },
{ "2160p30", bmdMode4K2160p30, "2160p30" },
{ "4k2160p30", bmdMode4K2160p30, "2160p30" },
{ "2160p50", bmdMode4K2160p50, "2160p50" },
{ "4k2160p50", bmdMode4K2160p50, "2160p50" },
{ "2160p5994", bmdMode4K2160p5994, "2160p59.94" },
{ "4k2160p5994", bmdMode4K2160p5994, "2160p59.94" },
{ "2160p60", bmdMode4K2160p60, "2160p60" },
{ "4k2160p60", bmdMode4K2160p60, "2160p60" }
{ bmdModeHD720p50, 1, 50 },
{ bmdModeHD720p5994, 1001, 60000 },
{ bmdModeHD720p60, 1, 60 },
{ bmdModeHD1080i50, 1, 25 },
{ bmdModeHD1080i5994, 1001, 30000 },
{ bmdModeHD1080i6000, 1, 30 },
{ bmdModeHD1080p2398, 1001, 24000 },
{ bmdModeHD1080p24, 1, 24 },
{ bmdModeHD1080p25, 1, 25 },
{ bmdModeHD1080p2997, 1001, 30000 },
{ bmdModeHD1080p30, 1, 30 },
{ bmdModeHD1080p50, 1, 50 },
{ bmdModeHD1080p5994, 1001, 60000 },
{ bmdModeHD1080p6000, 1, 60 },
{ bmdMode4K2160p2398, 1001, 24000 },
{ bmdMode4K2160p24, 1, 24 },
{ bmdMode4K2160p25, 1, 25 },
{ bmdMode4K2160p2997, 1001, 30000 },
{ bmdMode4K2160p30, 1, 30 },
{ bmdMode4K2160p50, 1, 50 },
{ bmdMode4K2160p5994, 1001, 60000 },
{ bmdMode4K2160p60, 1, 60 }
};
for (const ModeOption& option : options)
for (const ModeRate& rate : rates)
{
if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token))
{
videoMode.displayMode = option.mode;
videoMode.displayName = option.displayName;
return true;
}
if (rate.mode == displayMode && rate.timeScale > 0)
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
}
return false;
}
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error)
{
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
{
error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
inputVideoFormat + " / " + inputFrameRate;
return false;
}
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
{
error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
outputVideoFormat + " / " + outputFrameRate;
return false;
}
return true;
return fallbackMilliseconds;
}
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)

View File

@@ -1,47 +1,11 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include "VideoMode.h"
#include <string>
struct FrameSize
{
unsigned width = 0;
unsigned height = 0;
bool IsEmpty() const { return width == 0 || height == 0; }
};
inline bool operator==(const FrameSize& left, const FrameSize& right)
{
return left.width == right.width && left.height == right.height;
}
inline bool operator!=(const FrameSize& left, const FrameSize& right)
{
return !(left == right);
}
struct VideoFormat
{
BMDDisplayMode displayMode = bmdModeHD1080p5994;
std::string displayName = "1080p59.94";
};
struct VideoFormatSelection
{
VideoFormat input;
VideoFormat output;
};
std::string NormalizeModeToken(const std::string& value);
bool DeckLinkDisplayModeForVideoFormat(const VideoFormat& videoMode, BMDDisplayMode& displayMode);
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName);
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode);
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error);
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode);

View File

@@ -86,10 +86,17 @@ bool DeckLinkInput::Initialize(const DeckLinkInputConfig& config, std::string& e
mConfig = config;
Log("decklink-input", "Initializing DeckLink input for " + config.videoFormat.displayName + ".");
BMDDisplayMode displayMode = bmdModeUnknown;
if (!DeckLinkDisplayModeForVideoFormat(config.videoFormat, displayMode))
{
error = "DeckLink input setup failed while mapping " + config.videoFormat.displayName + " to a DeckLink display mode.";
return false;
}
if (!DiscoverInput(config, error))
return false;
if (mInput->EnableVideoInput(config.videoFormat.displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK)
if (mInput->EnableVideoInput(displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK)
{
error = "DeckLink input setup failed while enabling " +
std::string(mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8") +
@@ -260,6 +267,13 @@ void DeckLinkInput::HandleFormatChanged()
bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string& error)
{
BMDDisplayMode targetDisplayMode = bmdModeUnknown;
if (!DeckLinkDisplayModeForVideoFormat(config.videoFormat, targetDisplayMode))
{
error = "DeckLink input discovery failed while mapping " + config.videoFormat.displayName + " to a DeckLink display mode.";
return false;
}
CComPtr<IDeckLinkIterator> iterator;
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&iterator));
if (FAILED(result))
@@ -275,8 +289,8 @@ bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string
if (deckLink->QueryInterface(IID_IDeckLinkInput, reinterpret_cast<void**>(&candidateInput)) == S_OK && candidateInput != nullptr)
{
CComPtr<IDeckLinkDisplayMode> displayMode;
if (FindInputDisplayMode(candidateInput, config.videoFormat.displayMode, &displayMode) &&
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitBGRA))
if (FindInputDisplayMode(candidateInput, targetDisplayMode, &displayMode) &&
SupportsInputFormat(candidateInput, targetDisplayMode, bmdFormat8BitBGRA))
{
mInput = candidateInput;
mCapturePixelFormat = bmdFormat8BitBGRA;
@@ -284,7 +298,7 @@ bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string
return true;
}
if (displayMode != nullptr &&
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitYUV))
SupportsInputFormat(candidateInput, targetDisplayMode, bmdFormat8BitYUV))
{
mInput = candidateInput;
mCapturePixelFormat = bmdFormat8BitYUV;

View File

@@ -169,6 +169,19 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
CComPtr<IDeckLinkIterator> deckLinkIterator;
CComPtr<IDeckLinkDisplayMode> inputMode;
CComPtr<IDeckLinkDisplayMode> outputMode;
BMDDisplayMode inputDisplayMode = bmdModeUnknown;
BMDDisplayMode outputDisplayMode = bmdModeUnknown;
if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode))
{
error = "Cannot map configured input mode to DeckLink BMDDisplayMode: " + videoModes.input.displayName;
return false;
}
if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
{
error = "Cannot map configured output mode to DeckLink BMDDisplayMode: " + videoModes.output.displayName;
return false;
}
mState.inputDisplayModeName = videoModes.input.displayName;
mState.outputDisplayModeName = videoModes.output.displayName;
@@ -250,7 +263,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
return false;
}
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, inputDisplayMode, &inputMode))
{
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
ReleaseResources();
@@ -266,7 +279,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
return false;
}
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, outputDisplayMode, &outputMode))
{
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
ReleaseResources();
@@ -304,14 +317,22 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
}
mState.formatStatusMessage.clear();
BMDDisplayMode inputDisplayMode = bmdModeUnknown;
BMDDisplayMode outputDisplayMode = bmdModeUnknown;
if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode) ||
!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
{
error = "DeckLink format selection failed while mapping configured video modes.";
return false;
}
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, inputDisplayMode, bmdFormat10BitYUV);
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
if (input != nullptr && !inputTenBitSupported)
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
const bool outputTenBitSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUV);
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUVA);
mState.outputPixelFormat = outputAlphaRequired
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
@@ -369,8 +390,14 @@ bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFor
return true;
}
BMDDisplayMode inputDisplayMode = bmdModeUnknown;
if (!DeckLinkDisplayModeForVideoFormat(inputVideoMode, inputDisplayMode))
{
error = "DeckLink input setup failed while mapping " + inputVideoMode.displayName + " to a DeckLink display mode.";
return false;
}
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
if (input->EnableVideoInput(inputDisplayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
{
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
{
@@ -378,7 +405,7 @@ bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFor
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
if (input->EnableVideoInput(inputDisplayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
{
std::ostringstream status;
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
@@ -417,7 +444,13 @@ bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoF
{
mOutputFrameCallback = std::move(callback);
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
BMDDisplayMode outputDisplayMode = bmdModeUnknown;
if (!DeckLinkDisplayModeForVideoFormat(outputVideoMode, outputDisplayMode))
{
error = "DeckLink output setup failed while mapping " + outputVideoMode.displayName + " to a DeckLink display mode.";
return false;
}
if (output->EnableVideoOutput(outputDisplayMode, bmdVideoOutputFlagDefault) != S_OK)
{
error = "DeckLink output setup failed while enabling video output.";
return false;

View File

@@ -20,13 +20,13 @@ add_video_shader_test(RenderCadenceCompositorTelemetryTests
add_video_shader_test(RenderCadenceCompositorFrameExchangeTests
"${SRC_DIR}/frames/SystemFrameExchange.cpp"
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
${VIDEO_FORMAT_SOURCES}
"${TEST_DIR}/RenderCadenceCompositorFrameExchangeTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorInputFrameMailboxTests
"${SRC_DIR}/frames/InputFrameMailbox.cpp"
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
${VIDEO_FORMAT_SOURCES}
"${TEST_DIR}/RenderCadenceCompositorInputFrameMailboxTests.cpp"
)
@@ -72,6 +72,7 @@ add_video_shader_test(RenderCadenceCompositorSupportedShaderCatalogTests
add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests
"${SRC_DIR}/app/AppConfig.cpp"
"${SRC_DIR}/app/AppConfigProvider.cpp"
${VIDEO_MODE_SOURCES}
"${SRC_DIR}/json/JsonWriter.cpp"
${RUNTIME_LAYER_SOURCES}
${RUNTIME_TEXT_SOURCES}
@@ -96,6 +97,8 @@ target_link_libraries(RenderCadenceCompositorHttpControlServerTests PRIVATE Ws2_
add_video_shader_test(RenderCadenceCompositorAppConfigProviderTests
"${SRC_DIR}/app/AppConfig.cpp"
"${SRC_DIR}/app/AppConfigProvider.cpp"
${VIDEO_MODE_SOURCES}
"${SRC_DIR}/video/decklink/DeckLinkDisplayMode.cpp"
${RUNTIME_JSON_SOURCES}
"${TEST_DIR}/RenderCadenceCompositorAppConfigProviderTests.cpp"
)
@@ -129,7 +132,7 @@ add_video_shader_test(Std140BufferTests
add_video_shader_test(VideoIOFormatTests
"${SRC_DIR}/video/decklink/DeckLinkVideoIOFormat.cpp"
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
${VIDEO_FORMAT_SOURCES}
"${TEST_DIR}/VideoIOFormatTests.cpp"
)
@@ -139,7 +142,7 @@ add_video_shader_test(VideoPlayoutSchedulerTests
)
add_video_shader_test(VideoOutputThreadTests
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
${VIDEO_FORMAT_SOURCES}
"${TEST_DIR}/VideoOutputThreadTests.cpp"
)
@@ -160,7 +163,7 @@ add_video_shader_test(RenderCadenceControllerTests
add_video_shader_test(SystemOutputFramePoolTests
"${SRC_DIR}/video/playout/SystemOutputFramePool.cpp"
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
${VIDEO_FORMAT_SOURCES}
"${TEST_DIR}/SystemOutputFramePoolTests.cpp"
)
@@ -170,7 +173,7 @@ add_video_shader_test(VideoBackendLifecycleTests
)
add_video_shader_test(VideoIODeviceFakeTests
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
${VIDEO_FORMAT_SOURCES}
"${TEST_DIR}/VideoIODeviceFakeTests.cpp"
)

View File

@@ -1,4 +1,5 @@
#include "AppConfigProvider.h"
#include "DeckLinkDisplayMode.h"
#include <chrono>
#include <filesystem>