Compare commits
3 Commits
521b3cb09e
...
93aa8fa981
| Author | SHA1 | Date | |
|---|---|---|---|
| 93aa8fa981 | |||
| 4e6b37304f | |||
| cec8b76f61 |
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"/config/runtime-host.json",
|
||||||
|
"/config/runtime-host.*.json"
|
||||||
|
],
|
||||||
|
"url": "./config/runtime-host.schema.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "./runtime-host.schema.json",
|
||||||
"shaderLibrary": "shaders",
|
"shaderLibrary": "shaders",
|
||||||
"serverPort": 8080,
|
"serverPort": 8080,
|
||||||
"oscBindAddress": "0.0.0.0",
|
"oscBindAddress": "0.0.0.0",
|
||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
"oscSmoothing": 0.18,
|
"oscSmoothing": 0.18,
|
||||||
"input": {
|
"input": {
|
||||||
"backend": "ndi",
|
"backend": "none",
|
||||||
"device": "AIDENLAPTOP (NVIDIA GeForce RTX 4090 Laptop GPU 1)",
|
"device": "AIDENLAPTOP (NVIDIA GeForce RTX 4090 Laptop GPU 1)",
|
||||||
"resolution": "1080p",
|
"resolution": "1080p",
|
||||||
"frameRate": "59.94"
|
"frameRate": "59.94"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"backend": "decklink",
|
"backend": "ndi",
|
||||||
"device": "default",
|
"device": "shader toys",
|
||||||
"resolution": "1080p",
|
"resolution": "1080p",
|
||||||
"frameRate": "59.94",
|
"frameRate": "59.94",
|
||||||
"keying": {
|
"keying": {
|
||||||
|
|||||||
211
config/runtime-host.schema.json
Normal file
211
config/runtime-host.schema.json
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://render-cadence.local/schemas/runtime-host.schema.json",
|
||||||
|
"title": "Render Cadence Runtime Host Configuration",
|
||||||
|
"description": "Startup configuration for the Render Cadence native host. This schema documents the settings currently read by AppConfigProvider.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Editor-only schema reference. The native host ignores this field."
|
||||||
|
},
|
||||||
|
"shaderLibrary": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "shaders",
|
||||||
|
"description": "Path to the shader package library directory. Relative paths are resolved from the process/repo working location."
|
||||||
|
},
|
||||||
|
"serverPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"default": 8080,
|
||||||
|
"description": "Preferred HTTP control server port."
|
||||||
|
},
|
||||||
|
"oscBindAddress": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "0.0.0.0",
|
||||||
|
"description": "OSC bind address reserved for the control surface. The current native host exposes this in state but does not start the OSC listener yet."
|
||||||
|
},
|
||||||
|
"oscPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"default": 9000,
|
||||||
|
"description": "OSC UDP port reserved for the control surface."
|
||||||
|
},
|
||||||
|
"oscSmoothing": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 0.18,
|
||||||
|
"description": "Reserved OSC smoothing amount exposed in runtime state."
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"$ref": "#/$defs/input"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"$ref": "#/$defs/output"
|
||||||
|
},
|
||||||
|
"autoReload": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "When true, the runtime control layer may automatically rescan/reload shader packages when requested by the app flow."
|
||||||
|
},
|
||||||
|
"maxTemporalHistoryFrames": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 12,
|
||||||
|
"description": "Maximum temporal history frames exposed to shaders that request history."
|
||||||
|
},
|
||||||
|
"previewEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Starts the optional preview window on its own thread."
|
||||||
|
},
|
||||||
|
"previewFps": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"default": 59.94,
|
||||||
|
"description": "Target repaint rate for the optional preview window. It does not change render/output cadence."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"shaderLibrary",
|
||||||
|
"serverPort",
|
||||||
|
"input",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"inputBackend": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"decklink",
|
||||||
|
"ndi",
|
||||||
|
"none",
|
||||||
|
"disabled",
|
||||||
|
"off"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputBackend": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"decklink",
|
||||||
|
"ndi",
|
||||||
|
"none",
|
||||||
|
"disabled",
|
||||||
|
"off"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"720p",
|
||||||
|
"1080i",
|
||||||
|
"1080p",
|
||||||
|
"2160p",
|
||||||
|
"4k",
|
||||||
|
"uhd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frameRate": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"23.98",
|
||||||
|
"24",
|
||||||
|
"25",
|
||||||
|
"29.97",
|
||||||
|
"30",
|
||||||
|
"50",
|
||||||
|
"59.94",
|
||||||
|
"60"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Video input backend configuration. DeckLink uses the configured resolution/frameRate as a capture mode. NDI adapts to the received source shape and logs if it differs from these expected values.",
|
||||||
|
"properties": {
|
||||||
|
"backend": {
|
||||||
|
"$ref": "#/$defs/inputBackend",
|
||||||
|
"default": "decklink",
|
||||||
|
"description": "Input backend. Use 'decklink' for Blackmagic capture, 'ndi' for NDI receive, or 'none'/'disabled'/'off' for black fallback input."
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "default",
|
||||||
|
"description": "Input device/source selector. DeckLink currently uses 'default'. NDI accepts 'default'/'auto' for the first discovered source or an exact NDI source name."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"$ref": "#/$defs/resolution",
|
||||||
|
"default": "1080p",
|
||||||
|
"description": "Expected input resolution/mode. For NDI this is advisory only; received source dimensions are used."
|
||||||
|
},
|
||||||
|
"frameRate": {
|
||||||
|
"$ref": "#/$defs/frameRate",
|
||||||
|
"default": "59.94",
|
||||||
|
"description": "Expected input frame rate. DeckLink uses this to resolve the hardware mode; NDI currently treats it as an expectation for logging/state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backend",
|
||||||
|
"device",
|
||||||
|
"resolution",
|
||||||
|
"frameRate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Video output backend configuration.",
|
||||||
|
"properties": {
|
||||||
|
"backend": {
|
||||||
|
"$ref": "#/$defs/outputBackend",
|
||||||
|
"default": "decklink",
|
||||||
|
"description": "Output backend. Use 'decklink' for Blackmagic playout, 'ndi' for NDI send, or 'none'/'disabled'/'off' to disable scheduled output."
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "default",
|
||||||
|
"description": "Output device selector. DeckLink currently uses the first compatible/default output device. NDI uses this as the advertised sender name; 'default' becomes 'Render Cadence'."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"$ref": "#/$defs/resolution",
|
||||||
|
"default": "1080p",
|
||||||
|
"description": "Output render and video mode resolution."
|
||||||
|
},
|
||||||
|
"frameRate": {
|
||||||
|
"$ref": "#/$defs/frameRate",
|
||||||
|
"default": "59.94",
|
||||||
|
"description": "Output render cadence and video mode frame rate."
|
||||||
|
},
|
||||||
|
"keying": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "DeckLink keying options.",
|
||||||
|
"properties": {
|
||||||
|
"external": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Requests DeckLink external keying when the selected output device supports it."
|
||||||
|
},
|
||||||
|
"alphaRequired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Requires alpha-capable output format support during DeckLink output setup."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"external",
|
||||||
|
"alphaRequired"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backend",
|
||||||
|
"device",
|
||||||
|
"resolution",
|
||||||
|
"frameRate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,8 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mOutput->RequiresPreroll())
|
||||||
|
{
|
||||||
Log("app", "Starting video output thread.");
|
Log("app", "Starting video output thread.");
|
||||||
if (!mOutputThread.Start())
|
if (!mOutputThread.Start())
|
||||||
{
|
{
|
||||||
@@ -172,7 +174,7 @@ private:
|
|||||||
Log("app", "Waiting for video output preroll frames.");
|
Log("app", "Waiting for video output preroll frames.");
|
||||||
if (!WaitForPreroll())
|
if (!WaitForPreroll())
|
||||||
{
|
{
|
||||||
DisableVideoOutput("Timed out waiting for DeckLink preroll frames.");
|
DisableVideoOutput("Timed out waiting for video output preroll frames.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +184,21 @@ private:
|
|||||||
DisableVideoOutput("Scheduled video playback failed: " + outputError);
|
DisableVideoOutput("Scheduled video playback failed: " + outputError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("app", "Starting video output backend without preroll.");
|
||||||
|
if (!mOutput->StartScheduledPlayback(outputError))
|
||||||
|
{
|
||||||
|
DisableVideoOutput("Video output failed to start: " + outputError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mOutputThread.Start())
|
||||||
|
{
|
||||||
|
DisableVideoOutput("Video output thread failed to start.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mVideoOutputEnabled = true;
|
mVideoOutputEnabled = true;
|
||||||
mVideoOutputStatus = mConfig.output.backend + " scheduled output running.";
|
mVideoOutputStatus = mConfig.output.backend + " scheduled output running.";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "video/decklink/DeckLinkInputThread.h"
|
#include "video/decklink/DeckLinkInputThread.h"
|
||||||
#include "video/decklink/DeckLinkOutput.h"
|
#include "video/decklink/DeckLinkOutput.h"
|
||||||
#include "video/ndi/NdiInput.h"
|
#include "video/ndi/NdiInput.h"
|
||||||
|
#include "video/ndi/NdiOutput.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -58,7 +59,7 @@ struct VideoInputBackendStartContext
|
|||||||
bool inputVideoModeResolved = false;
|
bool inputVideoModeResolved = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
using VideoOutputBackendCreateFn = std::unique_ptr<IVideoOutputEdge> (*)();
|
using VideoOutputBackendCreateFn = std::unique_ptr<IVideoOutputEdge> (*)(const AppConfig&);
|
||||||
using VideoInputBackendStartFn = std::unique_ptr<VideoInputBackendSession> (*)(const VideoInputBackendStartContext&);
|
using VideoInputBackendStartFn = std::unique_ptr<VideoInputBackendSession> (*)(const VideoInputBackendStartContext&);
|
||||||
|
|
||||||
struct VideoOutputBackendRegistration
|
struct VideoOutputBackendRegistration
|
||||||
@@ -272,16 +273,21 @@ private:
|
|||||||
bool mStarted = false;
|
bool mStarted = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IVideoOutputEdge> CreateDeckLinkOutputBackend()
|
std::unique_ptr<IVideoOutputEdge> CreateDeckLinkOutputBackend(const AppConfig&)
|
||||||
{
|
{
|
||||||
return std::make_unique<DeckLinkOutput>();
|
return std::make_unique<DeckLinkOutput>();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<IVideoOutputEdge> CreateDisabledOutputBackend()
|
std::unique_ptr<IVideoOutputEdge> CreateDisabledOutputBackend(const AppConfig&)
|
||||||
{
|
{
|
||||||
return std::make_unique<DisabledVideoOutputEdge>("Video output backend is disabled by config.");
|
return std::make_unique<DisabledVideoOutputEdge>("Video output backend is disabled by config.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IVideoOutputEdge> CreateNdiOutputBackend(const AppConfig& config)
|
||||||
|
{
|
||||||
|
return std::make_unique<NdiOutput>(config.output.device);
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<VideoInputBackendSession> StartNoInputBackend(const VideoInputBackendStartContext&)
|
std::unique_ptr<VideoInputBackendSession> StartNoInputBackend(const VideoInputBackendStartContext&)
|
||||||
{
|
{
|
||||||
Log("app", "Video input backend disabled by config.");
|
Log("app", "Video input backend disabled by config.");
|
||||||
@@ -322,6 +328,7 @@ const std::vector<VideoOutputBackendRegistration>& VideoOutputBackendRegistry()
|
|||||||
{
|
{
|
||||||
static const std::vector<VideoOutputBackendRegistration> registry = {
|
static const std::vector<VideoOutputBackendRegistration> registry = {
|
||||||
VideoOutputBackendRegistration{ { "decklink" }, true, &CreateDeckLinkOutputBackend },
|
VideoOutputBackendRegistration{ { "decklink" }, true, &CreateDeckLinkOutputBackend },
|
||||||
|
VideoOutputBackendRegistration{ { "ndi" }, false, &CreateNdiOutputBackend },
|
||||||
VideoOutputBackendRegistration{ { "none", "disabled", "off" }, false, &CreateDisabledOutputBackend },
|
VideoOutputBackendRegistration{ { "none", "disabled", "off" }, false, &CreateDisabledOutputBackend },
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
@@ -372,7 +379,7 @@ std::unique_ptr<IVideoOutputEdge> CreateVideoOutputBackend(const AppConfig& conf
|
|||||||
const std::string backendName = NormalizeBackendName(config.output.backend);
|
const std::string backendName = NormalizeBackendName(config.output.backend);
|
||||||
const VideoOutputBackendRegistration* backend = FindVideoOutputBackend(backendName);
|
const VideoOutputBackendRegistration* backend = FindVideoOutputBackend(backendName);
|
||||||
if (backend != nullptr && backend->create != nullptr)
|
if (backend != nullptr && backend->create != nullptr)
|
||||||
return backend->create();
|
return backend->create(config);
|
||||||
|
|
||||||
return std::make_unique<DisabledVideoOutputEdge>("Unsupported output backend: " + config.output.backend);
|
return std::make_unique<DisabledVideoOutputEdge>("Unsupported output backend: " + config.output.backend);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public:
|
|||||||
virtual bool Initialize(const VideoOutputEdgeConfig& config, CompletionCallback completionCallback, std::string& error) = 0;
|
virtual bool Initialize(const VideoOutputEdgeConfig& config, CompletionCallback completionCallback, std::string& error) = 0;
|
||||||
virtual bool StartScheduledPlayback(std::string& error) = 0;
|
virtual bool StartScheduledPlayback(std::string& error) = 0;
|
||||||
virtual bool ScheduleFrame(const VideoIOOutputFrame& frame) = 0;
|
virtual bool ScheduleFrame(const VideoIOOutputFrame& frame) = 0;
|
||||||
|
virtual bool RequiresPreroll() const { return true; }
|
||||||
virtual void Stop() = 0;
|
virtual void Stop() = 0;
|
||||||
virtual void ReleaseResources() = 0;
|
virtual void ReleaseResources() = 0;
|
||||||
virtual const VideoIOState& State() const = 0;
|
virtual const VideoIOState& State() const = 0;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#include "NdiInput.h"
|
#include "NdiInput.h"
|
||||||
|
|
||||||
|
#include "NdiInputFormat.h"
|
||||||
|
#include "NdiRuntime.h"
|
||||||
#include "logging/Logger.h"
|
#include "logging/Logger.h"
|
||||||
#include "video/core/VideoIOFormat.h"
|
#include "video/core/VideoIOFormat.h"
|
||||||
|
|
||||||
#include <Processing.NDI.Lib.h>
|
#include <Processing.NDI.Lib.h>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -17,59 +18,11 @@ namespace
|
|||||||
constexpr std::chrono::milliseconds kDiscoveryRetry(500);
|
constexpr std::chrono::milliseconds kDiscoveryRetry(500);
|
||||||
constexpr uint32_t kCaptureTimeoutMilliseconds = 250;
|
constexpr uint32_t kCaptureTimeoutMilliseconds = 250;
|
||||||
|
|
||||||
std::mutex gNdiRuntimeMutex;
|
|
||||||
unsigned gNdiRuntimeUsers = 0;
|
|
||||||
|
|
||||||
bool AcquireNdiRuntime()
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(gNdiRuntimeMutex);
|
|
||||||
if (gNdiRuntimeUsers == 0 && !NDIlib_initialize())
|
|
||||||
return false;
|
|
||||||
++gNdiRuntimeUsers;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReleaseNdiRuntime()
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(gNdiRuntimeMutex);
|
|
||||||
if (gNdiRuntimeUsers == 0)
|
|
||||||
return;
|
|
||||||
--gNdiRuntimeUsers;
|
|
||||||
if (gNdiRuntimeUsers == 0)
|
|
||||||
NDIlib_destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsDefaultDeviceName(const std::string& device)
|
bool IsDefaultDeviceName(const std::string& device)
|
||||||
{
|
{
|
||||||
return device.empty() || device == "default" || device == "auto";
|
return device.empty() || device == "default" || device == "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string FourCcName(NDIlib_FourCC_video_type_e fourCc)
|
|
||||||
{
|
|
||||||
char text[5] = {};
|
|
||||||
const auto value = static_cast<uint32_t>(fourCc);
|
|
||||||
text[0] = static_cast<char>(value & 0xff);
|
|
||||||
text[1] = static_cast<char>((value >> 8) & 0xff);
|
|
||||||
text[2] = static_cast<char>((value >> 16) & 0xff);
|
|
||||||
text[3] = static_cast<char>((value >> 24) & 0xff);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MapNdiPixelFormat(NDIlib_FourCC_video_type_e fourCc, VideoIOPixelFormat& pixelFormat)
|
|
||||||
{
|
|
||||||
switch (fourCc)
|
|
||||||
{
|
|
||||||
case NDIlib_FourCC_video_type_BGRA:
|
|
||||||
case NDIlib_FourCC_video_type_BGRX:
|
|
||||||
pixelFormat = VideoIOPixelFormat::Bgra8;
|
|
||||||
return true;
|
|
||||||
case NDIlib_FourCC_video_type_UYVY:
|
|
||||||
pixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NdiInput::NdiInput(InputFrameMailbox& mailbox) :
|
NdiInput::NdiInput(InputFrameMailbox& mailbox) :
|
||||||
@@ -262,12 +215,12 @@ void NdiInput::HandleVideoFrame(void*, const void* frame)
|
|||||||
}
|
}
|
||||||
|
|
||||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
if (!MapNdiPixelFormat(videoFrame.FourCC, pixelFormat))
|
if (!MapNdiFourCcToVideoIOPixelFormat(videoFrame.FourCC, pixelFormat))
|
||||||
{
|
{
|
||||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
bool expected = false;
|
bool expected = false;
|
||||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
TryLog(LogLevel::Warning, "ndi-input", "Unsupported NDI input frame format " + FourCcName(videoFrame.FourCC) + ".");
|
TryLog(LogLevel::Warning, "ndi-input", std::string("Unsupported NDI input frame format ") + NdiFourCcName(videoFrame.FourCC) + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
src/video/ndi/NdiInputFormat.cpp
Normal file
35
src/video/ndi/NdiInputFormat.cpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#include "NdiInputFormat.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
const char* NdiFourCcName(NDIlib_FourCC_video_type_e fourCc)
|
||||||
|
{
|
||||||
|
static thread_local std::array<char, 5> text = {};
|
||||||
|
const auto value = static_cast<uint32_t>(fourCc);
|
||||||
|
text[0] = static_cast<char>(value & 0xff);
|
||||||
|
text[1] = static_cast<char>((value >> 8) & 0xff);
|
||||||
|
text[2] = static_cast<char>((value >> 16) & 0xff);
|
||||||
|
text[3] = static_cast<char>((value >> 24) & 0xff);
|
||||||
|
text[4] = '\0';
|
||||||
|
return text.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MapNdiFourCcToVideoIOPixelFormat(NDIlib_FourCC_video_type_e fourCc, VideoIOPixelFormat& pixelFormat)
|
||||||
|
{
|
||||||
|
switch (fourCc)
|
||||||
|
{
|
||||||
|
case NDIlib_FourCC_video_type_BGRA:
|
||||||
|
case NDIlib_FourCC_video_type_BGRX:
|
||||||
|
pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
return true;
|
||||||
|
case NDIlib_FourCC_video_type_UYVY:
|
||||||
|
pixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/video/ndi/NdiInputFormat.h
Normal file
11
src/video/ndi/NdiInputFormat.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "video/core/VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <Processing.NDI.Lib.h>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
const char* NdiFourCcName(NDIlib_FourCC_video_type_e fourCc);
|
||||||
|
bool MapNdiFourCcToVideoIOPixelFormat(NDIlib_FourCC_video_type_e fourCc, VideoIOPixelFormat& pixelFormat);
|
||||||
|
}
|
||||||
253
src/video/ndi/NdiOutput.cpp
Normal file
253
src/video/ndi/NdiOutput.cpp
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
#include "NdiOutput.h"
|
||||||
|
|
||||||
|
#include "NdiRuntime.h"
|
||||||
|
#include "logging/Logger.h"
|
||||||
|
#include "video/core/VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <Processing.NDI.Lib.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string ResolveSenderName(const std::string& configuredName)
|
||||||
|
{
|
||||||
|
if (configuredName.empty() || configuredName == "default" || configuredName == "auto")
|
||||||
|
return "Render Cadence";
|
||||||
|
return configuredName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetCommonVideoFrameFields(NDIlib_video_frame_v2_t& ndiFrame, const VideoIOOutputFrame& frame, const VideoOutputEdgeConfig& config)
|
||||||
|
{
|
||||||
|
std::memset(&ndiFrame, 0, sizeof(ndiFrame));
|
||||||
|
ndiFrame.xres = static_cast<int>(frame.width);
|
||||||
|
ndiFrame.yres = static_cast<int>(frame.height);
|
||||||
|
ndiFrame.FourCC = NDIlib_FourCC_video_type_BGRA;
|
||||||
|
ndiFrame.frame_rate_N = 60000;
|
||||||
|
ndiFrame.frame_rate_D = 1001;
|
||||||
|
if (config.outputVideoMode.frameRate == "60")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 60;
|
||||||
|
ndiFrame.frame_rate_D = 1;
|
||||||
|
}
|
||||||
|
else if (config.outputVideoMode.frameRate == "50")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 50;
|
||||||
|
ndiFrame.frame_rate_D = 1;
|
||||||
|
}
|
||||||
|
else if (config.outputVideoMode.frameRate == "30")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 30;
|
||||||
|
ndiFrame.frame_rate_D = 1;
|
||||||
|
}
|
||||||
|
else if (config.outputVideoMode.frameRate == "29.97")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 30000;
|
||||||
|
ndiFrame.frame_rate_D = 1001;
|
||||||
|
}
|
||||||
|
else if (config.outputVideoMode.frameRate == "25")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 25;
|
||||||
|
ndiFrame.frame_rate_D = 1;
|
||||||
|
}
|
||||||
|
else if (config.outputVideoMode.frameRate == "24")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 24;
|
||||||
|
ndiFrame.frame_rate_D = 1;
|
||||||
|
}
|
||||||
|
else if (config.outputVideoMode.frameRate == "23.98")
|
||||||
|
{
|
||||||
|
ndiFrame.frame_rate_N = 24000;
|
||||||
|
ndiFrame.frame_rate_D = 1001;
|
||||||
|
}
|
||||||
|
ndiFrame.picture_aspect_ratio = frame.height > 0 ? static_cast<float>(frame.width) / static_cast<float>(frame.height) : 0.0f;
|
||||||
|
ndiFrame.frame_format_type = NDIlib_frame_format_type_progressive;
|
||||||
|
ndiFrame.timecode = NDIlib_send_timecode_synthesize;
|
||||||
|
ndiFrame.p_data = static_cast<uint8_t*>(frame.bytes);
|
||||||
|
ndiFrame.line_stride_in_bytes = static_cast<int>(frame.rowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NdiOutput::NdiOutput(std::string senderName) :
|
||||||
|
mSenderName(ResolveSenderName(senderName))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
NdiOutput::~NdiOutput()
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiOutput::Initialize(const VideoOutputEdgeConfig& config, CompletionCallback completionCallback, std::string& error)
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
mConfig = config;
|
||||||
|
mCompletionCallback = std::move(completionCallback);
|
||||||
|
PopulateState(mState, config, mSenderName);
|
||||||
|
|
||||||
|
if (config.outputAlphaRequired)
|
||||||
|
{
|
||||||
|
mState.statusMessage = "NDI output sends BGRA frames with alpha when present; output.keying.alphaRequired is a DeckLink-only requirement.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AcquireNdiRuntime())
|
||||||
|
{
|
||||||
|
error = "NDI runtime initialization failed. The CPU/runtime might not support NDI.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NDIlib_send_create_t createSettings;
|
||||||
|
std::memset(&createSettings, 0, sizeof(createSettings));
|
||||||
|
createSettings.p_ndi_name = mSenderName.c_str();
|
||||||
|
createSettings.clock_video = false;
|
||||||
|
createSettings.clock_audio = false;
|
||||||
|
|
||||||
|
mSender = NDIlib_send_create(&createSettings);
|
||||||
|
if (mSender == nullptr)
|
||||||
|
{
|
||||||
|
ReleaseNdiRuntime();
|
||||||
|
error = "NDI sender creation failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mInitialized.store(true, std::memory_order_release);
|
||||||
|
mState.statusMessage = "NDI output sender '" + mSenderName + "' initialized.";
|
||||||
|
Log("ndi-output", mState.statusMessage);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiOutput::StartScheduledPlayback(std::string& error)
|
||||||
|
{
|
||||||
|
if (!mInitialized.load(std::memory_order_acquire) || mSender == nullptr)
|
||||||
|
{
|
||||||
|
error = "NDI output has not been initialized.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mRunning.store(true, std::memory_order_release);
|
||||||
|
Log("ndi-output", "NDI output sender started.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
|
||||||
|
{
|
||||||
|
if (!mRunning.load(std::memory_order_acquire) || frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8)
|
||||||
|
{
|
||||||
|
++mScheduleFailures;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NDIlib_video_frame_v2_t ndiFrame;
|
||||||
|
SetCommonVideoFrameFields(ndiFrame, frame, mConfig);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mSender == nullptr)
|
||||||
|
{
|
||||||
|
++mScheduleFailures;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||||
|
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(mSender), &ndiFrame);
|
||||||
|
mScheduleCallMilliseconds.store(
|
||||||
|
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(std::chrono::steady_clock::now() - scheduleStart).count(),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
|
||||||
|
CompletePendingLocked(VideoIOCompletionResult::Completed);
|
||||||
|
mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiOutput::Stop()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mSender != nullptr)
|
||||||
|
FlushPendingLocked();
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiOutput::ReleaseResources()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mSender != nullptr)
|
||||||
|
{
|
||||||
|
FlushPendingLocked();
|
||||||
|
NDIlib_send_destroy(static_cast<NDIlib_send_instance_t>(mSender));
|
||||||
|
mSender = nullptr;
|
||||||
|
ReleaseNdiRuntime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mInitialized.store(false, std::memory_order_release);
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
NdiOutputMetrics NdiOutput::Metrics() const
|
||||||
|
{
|
||||||
|
NdiOutputMetrics metrics;
|
||||||
|
metrics.completions = mCompletions.load(std::memory_order_relaxed);
|
||||||
|
metrics.dropped = mDropped.load(std::memory_order_relaxed);
|
||||||
|
metrics.flushed = mFlushed.load(std::memory_order_relaxed);
|
||||||
|
metrics.scheduleFailures = mScheduleFailures.load(std::memory_order_relaxed);
|
||||||
|
metrics.scheduleCallMilliseconds = mScheduleCallMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.actualBufferedFramesAvailable = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
metrics.actualBufferedFrames = mPendingBuffer != nullptr ? 1 : 0;
|
||||||
|
}
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiOutput::CompletePendingLocked(VideoIOCompletionResult result)
|
||||||
|
{
|
||||||
|
if (mPendingBuffer == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
VideoIOCompletion completion;
|
||||||
|
completion.result = result;
|
||||||
|
completion.outputFrameBuffer = mPendingBuffer;
|
||||||
|
mPendingBuffer = nullptr;
|
||||||
|
++mCompletions;
|
||||||
|
if (result == VideoIOCompletionResult::Dropped)
|
||||||
|
++mDropped;
|
||||||
|
else if (result == VideoIOCompletionResult::Flushed)
|
||||||
|
++mFlushed;
|
||||||
|
|
||||||
|
if (mCompletionCallback)
|
||||||
|
mCompletionCallback(completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiOutput::FlushPendingLocked()
|
||||||
|
{
|
||||||
|
if (mSender != nullptr)
|
||||||
|
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(mSender), nullptr);
|
||||||
|
CompletePendingLocked(VideoIOCompletionResult::Flushed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiOutput::PopulateState(VideoIOState& state, const VideoOutputEdgeConfig& config, const std::string& senderName)
|
||||||
|
{
|
||||||
|
state = VideoIOState();
|
||||||
|
state.outputFrameSize = config.outputVideoMode.frameSize;
|
||||||
|
state.inputFrameSize = state.outputFrameSize;
|
||||||
|
state.outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
state.inputPixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
state.outputFrameRowBytes = VideoIORowBytes(state.outputPixelFormat, state.outputFrameSize.width);
|
||||||
|
state.inputFrameRowBytes = state.outputFrameRowBytes;
|
||||||
|
state.outputPackTextureWidth = state.outputFrameSize.width;
|
||||||
|
state.captureTextureWidth = state.inputFrameSize.width;
|
||||||
|
state.outputDisplayModeName = config.outputVideoMode.displayName;
|
||||||
|
state.inputDisplayModeName = "No input - NDI output session";
|
||||||
|
state.outputModelName = "NDI: " + senderName;
|
||||||
|
state.hasInputDevice = false;
|
||||||
|
state.hasInputSource = false;
|
||||||
|
state.supportsExternalKeying = false;
|
||||||
|
state.supportsInternalKeying = false;
|
||||||
|
state.keyerInterfaceAvailable = false;
|
||||||
|
state.externalKeyingActive = false;
|
||||||
|
state.frameBudgetMilliseconds = config.outputVideoMode.frameDurationMilliseconds;
|
||||||
|
state.formatStatusMessage = "NDI output format: BGRA8.";
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/video/ndi/NdiOutput.h
Normal file
52
src/video/ndi/NdiOutput.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "video/core/VideoIOEdges.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
using NdiOutputMetrics = VideoOutputEdgeMetrics;
|
||||||
|
|
||||||
|
class NdiOutput final : public IVideoOutputEdge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NdiOutput(std::string senderName);
|
||||||
|
NdiOutput(const NdiOutput&) = delete;
|
||||||
|
NdiOutput& operator=(const NdiOutput&) = delete;
|
||||||
|
~NdiOutput() override;
|
||||||
|
|
||||||
|
bool Initialize(const VideoOutputEdgeConfig& config, CompletionCallback completionCallback, std::string& error) override;
|
||||||
|
bool StartScheduledPlayback(std::string& error) override;
|
||||||
|
bool ScheduleFrame(const VideoIOOutputFrame& frame) override;
|
||||||
|
bool RequiresPreroll() const override { return false; }
|
||||||
|
void Stop() override;
|
||||||
|
void ReleaseResources() override;
|
||||||
|
|
||||||
|
const VideoIOState& State() const override { return mState; }
|
||||||
|
NdiOutputMetrics Metrics() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void CompletePendingLocked(VideoIOCompletionResult result);
|
||||||
|
void FlushPendingLocked();
|
||||||
|
static void PopulateState(VideoIOState& state, const VideoOutputEdgeConfig& config, const std::string& senderName);
|
||||||
|
|
||||||
|
std::string mSenderName;
|
||||||
|
CompletionCallback mCompletionCallback;
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
void* mSender = nullptr;
|
||||||
|
void* mPendingBuffer = nullptr;
|
||||||
|
VideoOutputEdgeConfig mConfig;
|
||||||
|
VideoIOState mState;
|
||||||
|
std::atomic<bool> mInitialized{ false };
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
std::atomic<uint64_t> mCompletions{ 0 };
|
||||||
|
std::atomic<uint64_t> mDropped{ 0 };
|
||||||
|
std::atomic<uint64_t> mFlushed{ 0 };
|
||||||
|
std::atomic<uint64_t> mScheduleFailures{ 0 };
|
||||||
|
std::atomic<double> mScheduleCallMilliseconds{ 0.0 };
|
||||||
|
};
|
||||||
|
}
|
||||||
33
src/video/ndi/NdiRuntime.cpp
Normal file
33
src/video/ndi/NdiRuntime.cpp
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#include "NdiRuntime.h"
|
||||||
|
|
||||||
|
#include <Processing.NDI.Lib.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::mutex gNdiRuntimeMutex;
|
||||||
|
unsigned gNdiRuntimeUsers = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AcquireNdiRuntime()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(gNdiRuntimeMutex);
|
||||||
|
if (gNdiRuntimeUsers == 0 && !NDIlib_initialize())
|
||||||
|
return false;
|
||||||
|
++gNdiRuntimeUsers;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseNdiRuntime()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(gNdiRuntimeMutex);
|
||||||
|
if (gNdiRuntimeUsers == 0)
|
||||||
|
return;
|
||||||
|
--gNdiRuntimeUsers;
|
||||||
|
if (gNdiRuntimeUsers == 0)
|
||||||
|
NDIlib_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/video/ndi/NdiRuntime.h
Normal file
7
src/video/ndi/NdiRuntime.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
bool AcquireNdiRuntime();
|
||||||
|
void ReleaseNdiRuntime();
|
||||||
|
}
|
||||||
@@ -136,6 +136,13 @@ add_video_shader_test(VideoIOFormatTests
|
|||||||
"${TEST_DIR}/VideoIOFormatTests.cpp"
|
"${TEST_DIR}/VideoIOFormatTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_video_shader_test(NdiInputFormatTests
|
||||||
|
"${SRC_DIR}/video/ndi/NdiInputFormat.cpp"
|
||||||
|
${VIDEO_FORMAT_SOURCES}
|
||||||
|
"${TEST_DIR}/NdiInputFormatTests.cpp"
|
||||||
|
)
|
||||||
|
target_include_directories(NdiInputFormatTests PRIVATE "${NDI_INCLUDE_DIR}")
|
||||||
|
|
||||||
add_video_shader_test(VideoPlayoutSchedulerTests
|
add_video_shader_test(VideoPlayoutSchedulerTests
|
||||||
"${SRC_DIR}/video/playout/VideoPlayoutScheduler.cpp"
|
"${SRC_DIR}/video/playout/VideoPlayoutScheduler.cpp"
|
||||||
"${TEST_DIR}/VideoPlayoutSchedulerTests.cpp"
|
"${TEST_DIR}/VideoPlayoutSchedulerTests.cpp"
|
||||||
|
|||||||
62
tests/NdiInputFormatTests.cpp
Normal file
62
tests/NdiInputFormatTests.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "video/ndi/NdiInputFormat.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestNdiFourCcMapping()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
VideoIOPixelFormat format = VideoIOPixelFormat::Uyvy8;
|
||||||
|
Expect(MapNdiFourCcToVideoIOPixelFormat(NDIlib_FourCC_video_type_BGRA, format), "BGRA maps successfully");
|
||||||
|
Expect(format == VideoIOPixelFormat::Bgra8, "BGRA maps to Bgra8");
|
||||||
|
|
||||||
|
format = VideoIOPixelFormat::Uyvy8;
|
||||||
|
Expect(MapNdiFourCcToVideoIOPixelFormat(NDIlib_FourCC_video_type_BGRX, format), "BGRX maps successfully");
|
||||||
|
Expect(format == VideoIOPixelFormat::Bgra8, "BGRX maps to Bgra8");
|
||||||
|
|
||||||
|
format = VideoIOPixelFormat::Bgra8;
|
||||||
|
Expect(MapNdiFourCcToVideoIOPixelFormat(NDIlib_FourCC_video_type_UYVY, format), "UYVY maps successfully");
|
||||||
|
Expect(format == VideoIOPixelFormat::Uyvy8, "UYVY maps to Uyvy8");
|
||||||
|
|
||||||
|
format = VideoIOPixelFormat::Bgra8;
|
||||||
|
Expect(!MapNdiFourCcToVideoIOPixelFormat(NDIlib_FourCC_video_type_RGBA, format), "RGBA is unsupported until conversion is implemented");
|
||||||
|
Expect(format == VideoIOPixelFormat::Bgra8, "unsupported mapping leaves output untouched");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestNdiFourCcNames()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
Expect(std::string(NdiFourCcName(NDIlib_FourCC_video_type_BGRA)) == "BGRA", "BGRA name is readable");
|
||||||
|
Expect(std::string(NdiFourCcName(NDIlib_FourCC_video_type_UYVY)) == "UYVY", "UYVY name is readable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestNdiFourCcMapping();
|
||||||
|
TestNdiFourCcNames();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " NdiInputFormat test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "NdiInputFormat tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user