NDI input
This commit is contained in:
@@ -12,6 +12,7 @@ set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
|||||||
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||||
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
||||||
set(MSDF_ATLAS_GEN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
|
set(MSDF_ATLAS_GEN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
|
||||||
|
set(NDI_SDK_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/NDI 6 SDK" CACHE PATH "Path to the NDI SDK")
|
||||||
|
|
||||||
set(VIDEO_SHADER_INCLUDE_DIRS
|
set(VIDEO_SHADER_INCLUDE_DIRS
|
||||||
"${SRC_DIR}"
|
"${SRC_DIR}"
|
||||||
@@ -38,6 +39,7 @@ set(VIDEO_SHADER_INCLUDE_DIRS
|
|||||||
"${SRC_DIR}/video"
|
"${SRC_DIR}/video"
|
||||||
"${SRC_DIR}/video/core"
|
"${SRC_DIR}/video/core"
|
||||||
"${SRC_DIR}/video/decklink"
|
"${SRC_DIR}/video/decklink"
|
||||||
|
"${SRC_DIR}/video/ndi"
|
||||||
"${SRC_DIR}/video/playout"
|
"${SRC_DIR}/video/playout"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,6 +131,11 @@ set(MSDF_ATLAS_GEN_RUNTIME_FILES
|
|||||||
)
|
)
|
||||||
set(MSDF_ATLAS_GEN_LICENSE_FILE "${MSDF_ATLAS_GEN_ROOT}/LICENSE.txt")
|
set(MSDF_ATLAS_GEN_LICENSE_FILE "${MSDF_ATLAS_GEN_ROOT}/LICENSE.txt")
|
||||||
set(MSDF_ATLAS_GEN_README_FILE "${MSDF_ATLAS_GEN_ROOT}/README.md")
|
set(MSDF_ATLAS_GEN_README_FILE "${MSDF_ATLAS_GEN_ROOT}/README.md")
|
||||||
|
set(NDI_INCLUDE_DIR "${NDI_SDK_ROOT}/Include")
|
||||||
|
set(NDI_RUNTIME_DLL "${NDI_SDK_ROOT}/Bin/x64/Processing.NDI.Lib.x64.dll")
|
||||||
|
set(NDI_IMPORT_LIB "${NDI_SDK_ROOT}/Lib/x64/Processing.NDI.Lib.x64.lib")
|
||||||
|
set(NDI_LICENSE_FILE "${NDI_SDK_ROOT}/NDI SDK License Agreement.pdf")
|
||||||
|
set(NDI_NOTICES_FILE "${NDI_SDK_ROOT}/Bin/x64/Processing.NDI.Lib.Licenses.txt")
|
||||||
|
|
||||||
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
||||||
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
||||||
@@ -150,6 +157,19 @@ else()
|
|||||||
)
|
)
|
||||||
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
||||||
video_shader_target_defaults(RenderCadenceCompositor)
|
video_shader_target_defaults(RenderCadenceCompositor)
|
||||||
|
if(EXISTS "${NDI_INCLUDE_DIR}" AND EXISTS "${NDI_IMPORT_LIB}")
|
||||||
|
target_include_directories(RenderCadenceCompositor PRIVATE "${NDI_INCLUDE_DIR}")
|
||||||
|
target_link_libraries(RenderCadenceCompositor PRIVATE "${NDI_IMPORT_LIB}")
|
||||||
|
if(EXISTS "${NDI_RUNTIME_DLL}")
|
||||||
|
add_custom_command(TARGET RenderCadenceCompositor POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${NDI_RUNTIME_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:RenderCadenceCompositor>/Processing.NDI.Lib.x64.dll"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
message(STATUS "NDI SDK headers/import library not found; NDI input backend will not build correctly: ${NDI_SDK_ROOT}")
|
||||||
|
endif()
|
||||||
target_link_libraries(RenderCadenceCompositor PRIVATE
|
target_link_libraries(RenderCadenceCompositor PRIVATE
|
||||||
opengl32
|
opengl32
|
||||||
Ole32
|
Ole32
|
||||||
@@ -216,6 +236,32 @@ else()
|
|||||||
message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}")
|
message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(EXISTS "${NDI_RUNTIME_DLL}")
|
||||||
|
install(FILES "${NDI_RUNTIME_DLL}"
|
||||||
|
DESTINATION "."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "NDI runtime DLL not found and will not be installed: ${NDI_RUNTIME_DLL}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(EXISTS "${NDI_LICENSE_FILE}")
|
||||||
|
install(FILES "${NDI_LICENSE_FILE}"
|
||||||
|
DESTINATION "third_party_notices"
|
||||||
|
RENAME "NDI_SDK_LICENSE_AGREEMENT.pdf"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "NDI license file not found: ${NDI_LICENSE_FILE}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(EXISTS "${NDI_NOTICES_FILE}")
|
||||||
|
install(FILES "${NDI_NOTICES_FILE}"
|
||||||
|
DESTINATION "third_party_notices"
|
||||||
|
RENAME "NDI_RUNTIME_LICENSES.txt"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "NDI runtime notices file not found: ${NDI_NOTICES_FILE}")
|
||||||
|
endif()
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
||||||
DESTINATION "."
|
DESTINATION "."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
"oscSmoothing": 0.18,
|
"oscSmoothing": 0.18,
|
||||||
"input": {
|
"input": {
|
||||||
"backend": "decklink",
|
"backend": "ndi",
|
||||||
"device": "default",
|
"device": "AIDENLAPTOP (NVIDIA GeForce RTX 4090 Laptop GPU 1)",
|
||||||
"resolution": "1080p",
|
"resolution": "1080p",
|
||||||
"frameRate": "59.94"
|
"frameRate": "59.94"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "video/decklink/DeckLinkInput.h"
|
#include "video/decklink/DeckLinkInput.h"
|
||||||
#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 <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -199,6 +200,78 @@ private:
|
|||||||
bool mStarted = false;
|
bool mStarted = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class NdiInputBackendSession final : public VideoInputBackendSession
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NdiInputBackendSession(InputFrameMailbox& mailbox) :
|
||||||
|
mInput(mailbox)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~NdiInputBackendSession() override
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Start(
|
||||||
|
const AppConfig& config,
|
||||||
|
InputFrameMailbox& mailbox,
|
||||||
|
InputFrameMailboxConfig& mailboxConfig,
|
||||||
|
const VideoFormat& configuredInputMode,
|
||||||
|
bool inputVideoModeResolved,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
NdiInputConfig inputConfig;
|
||||||
|
inputConfig.device = config.input.device;
|
||||||
|
inputConfig.expectedFrameRate = config.input.frameRate;
|
||||||
|
inputConfig.mailboxCapacity = mailboxConfig.capacity;
|
||||||
|
inputConfig.maxReadyFrames = mailboxConfig.maxReadyFrames;
|
||||||
|
VideoFormatDimensions(config.input.resolution, inputConfig.expectedWidth, inputConfig.expectedHeight);
|
||||||
|
if (inputVideoModeResolved)
|
||||||
|
{
|
||||||
|
inputConfig.expectedWidth = configuredInputMode.frameSize.width;
|
||||||
|
inputConfig.expectedHeight = configuredInputMode.frameSize.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mInput.Initialize(inputConfig, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!mInput.Start(error))
|
||||||
|
{
|
||||||
|
mInput.ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mStarted = true;
|
||||||
|
Log("app", "NDI input edge started; it will adapt InputFrameMailbox to the received source shape.");
|
||||||
|
if (inputConfig.expectedWidth > 0 && inputConfig.expectedHeight > 0)
|
||||||
|
{
|
||||||
|
Log(
|
||||||
|
"app",
|
||||||
|
"Configured NDI input expectation is " + std::to_string(inputConfig.expectedWidth) + "x" +
|
||||||
|
std::to_string(inputConfig.expectedHeight) + " at " + config.input.frameRate + " fps.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop() override
|
||||||
|
{
|
||||||
|
if (mStarted)
|
||||||
|
mInput.Stop();
|
||||||
|
mInput.ReleaseResources();
|
||||||
|
mStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Started() const override { return mStarted; }
|
||||||
|
VideoInputEdgeMetrics Metrics() const override { return mInput.Metrics(); }
|
||||||
|
const std::string& BackendName() const override { return mBackendName; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string mBackendName = "ndi";
|
||||||
|
NdiInput mInput;
|
||||||
|
bool mStarted = false;
|
||||||
|
};
|
||||||
|
|
||||||
std::unique_ptr<IVideoOutputEdge> CreateDeckLinkOutputBackend()
|
std::unique_ptr<IVideoOutputEdge> CreateDeckLinkOutputBackend()
|
||||||
{
|
{
|
||||||
return std::make_unique<DeckLinkOutput>();
|
return std::make_unique<DeckLinkOutput>();
|
||||||
@@ -233,6 +306,18 @@ std::unique_ptr<VideoInputBackendSession> StartDeckLinkInputBackend(const VideoI
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<VideoInputBackendSession> StartNdiInputBackend(const VideoInputBackendStartContext& context)
|
||||||
|
{
|
||||||
|
auto session = std::make_unique<NdiInputBackendSession>(context.mailbox);
|
||||||
|
std::string error;
|
||||||
|
if (!session->Start(context.config, context.mailbox, context.mailboxConfig, context.inputVideoMode, context.inputVideoModeResolved, error))
|
||||||
|
{
|
||||||
|
LogWarning("app", "NDI input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + error);
|
||||||
|
return std::make_unique<NoInputBackendSession>("ndi");
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
const std::vector<VideoOutputBackendRegistration>& VideoOutputBackendRegistry()
|
const std::vector<VideoOutputBackendRegistration>& VideoOutputBackendRegistry()
|
||||||
{
|
{
|
||||||
static const std::vector<VideoOutputBackendRegistration> registry = {
|
static const std::vector<VideoOutputBackendRegistration> registry = {
|
||||||
@@ -246,6 +331,7 @@ const std::vector<VideoInputBackendRegistration>& VideoInputBackendRegistry()
|
|||||||
{
|
{
|
||||||
static const std::vector<VideoInputBackendRegistration> registry = {
|
static const std::vector<VideoInputBackendRegistration> registry = {
|
||||||
VideoInputBackendRegistration{ { "decklink" }, true, &StartDeckLinkInputBackend },
|
VideoInputBackendRegistration{ { "decklink" }, true, &StartDeckLinkInputBackend },
|
||||||
|
VideoInputBackendRegistration{ { "ndi" }, false, &StartNdiInputBackend },
|
||||||
VideoInputBackendRegistration{ { "none", "disabled", "off" }, false, &StartNoInputBackend },
|
VideoInputBackendRegistration{ { "none", "disabled", "off" }, false, &StartNoInputBackend },
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
|
|||||||
363
src/video/ndi/NdiInput.cpp
Normal file
363
src/video/ndi/NdiInput.cpp
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
#include "NdiInput.h"
|
||||||
|
|
||||||
|
#include "logging/Logger.h"
|
||||||
|
#include "video/core/VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <Processing.NDI.Lib.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr std::chrono::milliseconds kDiscoveryRetry(500);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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) :
|
||||||
|
mMailbox(mailbox)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
NdiInput::~NdiInput()
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiInput::Initialize(const NdiInputConfig& config, std::string& error)
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
mConfig = config;
|
||||||
|
if (!AcquireNdiRuntime())
|
||||||
|
{
|
||||||
|
error = "NDI runtime initialization failed. The CPU/runtime might not support NDI.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mInitialized.store(true, std::memory_order_release);
|
||||||
|
Log("ndi-input", "NDI input edge initialized for device '" + (IsDefaultDeviceName(config.device) ? std::string("default") : config.device) + "'.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiInput::Start(std::string& error)
|
||||||
|
{
|
||||||
|
if (!mInitialized.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
error = "NDI input has not been initialized.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mThread.joinable())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
mStopping.store(false, std::memory_order_release);
|
||||||
|
mRunning.store(true, std::memory_order_release);
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
Log("ndi-input", "NDI input capture thread started.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::Stop()
|
||||||
|
{
|
||||||
|
mStopping.store(true, std::memory_order_release);
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
DisconnectReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::ReleaseResources()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
if (mInitialized.exchange(false, std::memory_order_acq_rel))
|
||||||
|
ReleaseNdiRuntime();
|
||||||
|
}
|
||||||
|
|
||||||
|
NdiInputMetrics NdiInput::Metrics() const
|
||||||
|
{
|
||||||
|
NdiInputMetrics metrics;
|
||||||
|
metrics.capturedFrames = mCapturedFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.noInputSourceFrames = mNoInputSourceFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.unsupportedFrames = mUnsupportedFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.submitMisses = mSubmitMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.convertMilliseconds = 0.0;
|
||||||
|
metrics.submitMilliseconds = mSubmitMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.captureFormat = CaptureFormatName();
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::ThreadMain()
|
||||||
|
{
|
||||||
|
while (!mStopping.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
if (mReceiver == nullptr && !ConnectToConfiguredSource())
|
||||||
|
{
|
||||||
|
mNoInputSourceFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedNoSource.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "ndi-input", "No NDI input source available yet; render input will use fallback until a source appears.");
|
||||||
|
std::this_thread::sleep_for(kDiscoveryRetry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NDIlib_video_frame_v2_t videoFrame;
|
||||||
|
const NDIlib_frame_type_e frameType = NDIlib_recv_capture_v3(
|
||||||
|
static_cast<NDIlib_recv_instance_t>(mReceiver),
|
||||||
|
&videoFrame,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
kCaptureTimeoutMilliseconds);
|
||||||
|
|
||||||
|
if (frameType == NDIlib_frame_type_video)
|
||||||
|
{
|
||||||
|
HandleVideoFrame(mReceiver, &videoFrame);
|
||||||
|
NDIlib_recv_free_video_v2(static_cast<NDIlib_recv_instance_t>(mReceiver), &videoFrame);
|
||||||
|
}
|
||||||
|
else if (frameType == NDIlib_frame_type_error)
|
||||||
|
{
|
||||||
|
TryLog(LogLevel::Warning, "ndi-input", "NDI input receiver reported an error; reconnecting.");
|
||||||
|
DisconnectReceiver();
|
||||||
|
}
|
||||||
|
else if (frameType == NDIlib_frame_type_source_change)
|
||||||
|
{
|
||||||
|
TryLog(LogLevel::Log, "ndi-input", "NDI input source changed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiInput::ConnectToConfiguredSource()
|
||||||
|
{
|
||||||
|
return IsDefaultDeviceName(mConfig.device) ? ConnectToDefaultSource() : [&]() {
|
||||||
|
NDIlib_source_t source;
|
||||||
|
std::memset(&source, 0, sizeof(source));
|
||||||
|
source.p_ndi_name = mConfig.device.c_str();
|
||||||
|
|
||||||
|
NDIlib_recv_create_v3_t createSettings;
|
||||||
|
std::memset(&createSettings, 0, sizeof(createSettings));
|
||||||
|
createSettings.source_to_connect_to = source;
|
||||||
|
createSettings.color_format = NDIlib_recv_color_format_BGRX_BGRA;
|
||||||
|
createSettings.bandwidth = NDIlib_recv_bandwidth_highest;
|
||||||
|
createSettings.allow_video_fields = false;
|
||||||
|
createSettings.p_ndi_recv_name = "Render Cadence NDI Input";
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mReceiverMutex);
|
||||||
|
mReceiver = NDIlib_recv_create_v3(&createSettings);
|
||||||
|
if (mReceiver != nullptr)
|
||||||
|
Log("ndi-input", "NDI input connected to configured source '" + mConfig.device + "'.");
|
||||||
|
return mReceiver != nullptr;
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiInput::ConnectToDefaultSource()
|
||||||
|
{
|
||||||
|
NDIlib_find_instance_t finder = NDIlib_find_create_v2();
|
||||||
|
if (finder == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint32_t sourceCount = 0;
|
||||||
|
const NDIlib_source_t* sources = NDIlib_find_get_current_sources(finder, &sourceCount);
|
||||||
|
if (sourceCount == 0)
|
||||||
|
{
|
||||||
|
NDIlib_find_wait_for_sources(finder, 100);
|
||||||
|
sources = NDIlib_find_get_current_sources(finder, &sourceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceCount == 0 || sources == nullptr)
|
||||||
|
{
|
||||||
|
NDIlib_find_destroy(finder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NDIlib_recv_create_v3_t createSettings;
|
||||||
|
std::memset(&createSettings, 0, sizeof(createSettings));
|
||||||
|
createSettings.source_to_connect_to = sources[0];
|
||||||
|
createSettings.color_format = NDIlib_recv_color_format_BGRX_BGRA;
|
||||||
|
createSettings.bandwidth = NDIlib_recv_bandwidth_highest;
|
||||||
|
createSettings.allow_video_fields = false;
|
||||||
|
createSettings.p_ndi_recv_name = "Render Cadence NDI Input";
|
||||||
|
|
||||||
|
const std::string sourceName = sources[0].p_ndi_name != nullptr ? sources[0].p_ndi_name : "unnamed";
|
||||||
|
std::lock_guard<std::mutex> lock(mReceiverMutex);
|
||||||
|
mReceiver = NDIlib_recv_create_v3(&createSettings);
|
||||||
|
NDIlib_find_destroy(finder);
|
||||||
|
if (mReceiver != nullptr)
|
||||||
|
Log("ndi-input", "NDI input connected to default source '" + sourceName + "'.");
|
||||||
|
return mReceiver != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::DisconnectReceiver()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mReceiverMutex);
|
||||||
|
if (mReceiver != nullptr)
|
||||||
|
{
|
||||||
|
NDIlib_recv_destroy(static_cast<NDIlib_recv_instance_t>(mReceiver));
|
||||||
|
mReceiver = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::HandleVideoFrame(void*, const void* frame)
|
||||||
|
{
|
||||||
|
const NDIlib_video_frame_v2_t& videoFrame = *static_cast<const NDIlib_video_frame_v2_t*>(frame);
|
||||||
|
if (videoFrame.p_data == nullptr || videoFrame.xres <= 0 || videoFrame.yres <= 0 || videoFrame.line_stride_in_bytes <= 0)
|
||||||
|
{
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
if (!MapNdiPixelFormat(videoFrame.FourCC, pixelFormat))
|
||||||
|
{
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "ndi-input", "Unsupported NDI input frame format " + FourCcName(videoFrame.FourCC) + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||||
|
if (SubmitFrame(
|
||||||
|
videoFrame.p_data,
|
||||||
|
static_cast<unsigned>(videoFrame.xres),
|
||||||
|
static_cast<unsigned>(videoFrame.yres),
|
||||||
|
static_cast<unsigned>(videoFrame.line_stride_in_bytes),
|
||||||
|
pixelFormat,
|
||||||
|
frameIndex))
|
||||||
|
{
|
||||||
|
mCapturedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
LogConfiguredShapeMismatchOnce(static_cast<unsigned>(videoFrame.xres), static_cast<unsigned>(videoFrame.yres));
|
||||||
|
LogFirstFrameOnce(static_cast<unsigned>(videoFrame.xres), static_cast<unsigned>(videoFrame.yres), pixelFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NdiInput::SubmitFrame(const void* bytes, unsigned width, unsigned height, unsigned rowBytes, VideoIOPixelFormat pixelFormat, uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
ConfigureMailboxIfNeeded(width, height, rowBytes, pixelFormat);
|
||||||
|
const auto submitStart = std::chrono::steady_clock::now();
|
||||||
|
const bool submitted = mMailbox.SubmitFrame(bytes, rowBytes, frameIndex);
|
||||||
|
mSubmitMilliseconds.store(
|
||||||
|
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(std::chrono::steady_clock::now() - submitStart).count(),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
if (!submitted)
|
||||||
|
{
|
||||||
|
mSubmitMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedSubmitMiss.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "ndi-input", "NDI input frame could not be submitted to InputFrameMailbox.");
|
||||||
|
}
|
||||||
|
return submitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::ConfigureMailboxIfNeeded(unsigned width, unsigned height, unsigned rowBytes, VideoIOPixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
InputFrameMailboxConfig current = mMailbox.Config();
|
||||||
|
if (current.width == width && current.height == height && current.rowBytes == rowBytes && current.pixelFormat == pixelFormat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
current.width = width;
|
||||||
|
current.height = height;
|
||||||
|
current.rowBytes = rowBytes;
|
||||||
|
current.pixelFormat = pixelFormat;
|
||||||
|
current.capacity = mConfig.mailboxCapacity;
|
||||||
|
current.maxReadyFrames = mConfig.maxReadyFrames;
|
||||||
|
mMailbox.Configure(current);
|
||||||
|
mCapturePixelFormat.store(pixelFormat, std::memory_order_release);
|
||||||
|
TryLog(
|
||||||
|
LogLevel::Log,
|
||||||
|
"ndi-input",
|
||||||
|
"NDI input mailbox adapted to " + std::to_string(width) + "x" + std::to_string(height) +
|
||||||
|
" " + VideoIOPixelFormatName(pixelFormat) + " rowBytes=" + std::to_string(rowBytes) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::LogConfiguredShapeMismatchOnce(unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
if (mConfig.expectedWidth == 0 || mConfig.expectedHeight == 0 ||
|
||||||
|
(width == mConfig.expectedWidth && height == mConfig.expectedHeight))
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedShapeMismatch.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
TryLog(
|
||||||
|
LogLevel::Warning,
|
||||||
|
"ndi-input",
|
||||||
|
"NDI input source is " + std::to_string(width) + "x" + std::to_string(height) +
|
||||||
|
", which differs from configured input " + std::to_string(mConfig.expectedWidth) + "x" +
|
||||||
|
std::to_string(mConfig.expectedHeight) + ". Adapting to the source shape.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NdiInput::LogFirstFrameOnce(unsigned width, unsigned height, VideoIOPixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedFirstFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
TryLog(
|
||||||
|
LogLevel::Log,
|
||||||
|
"ndi-input",
|
||||||
|
"First NDI input frame submitted to InputFrameMailbox: " + std::to_string(width) + "x" +
|
||||||
|
std::to_string(height) + " " + VideoIOPixelFormatName(pixelFormat) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* NdiInput::CaptureFormatName() const
|
||||||
|
{
|
||||||
|
return VideoIOPixelFormatName(mCapturePixelFormat.load(std::memory_order_acquire));
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/video/ndi/NdiInput.h
Normal file
77
src/video/ndi/NdiInput.h
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "frames/InputFrameMailbox.h"
|
||||||
|
#include "video/core/VideoIOEdges.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct NdiInputConfig
|
||||||
|
{
|
||||||
|
std::string device = "default";
|
||||||
|
unsigned expectedWidth = 0;
|
||||||
|
unsigned expectedHeight = 0;
|
||||||
|
std::string expectedFrameRate;
|
||||||
|
std::size_t mailboxCapacity = 4;
|
||||||
|
std::size_t maxReadyFrames = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
using NdiInputMetrics = VideoInputEdgeMetrics;
|
||||||
|
|
||||||
|
class NdiInput final : public IVideoInputEdge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NdiInput(InputFrameMailbox& mailbox);
|
||||||
|
NdiInput(const NdiInput&) = delete;
|
||||||
|
NdiInput& operator=(const NdiInput&) = delete;
|
||||||
|
~NdiInput() override;
|
||||||
|
|
||||||
|
bool Initialize(const NdiInputConfig& config, std::string& error);
|
||||||
|
bool Start(std::string& error) override;
|
||||||
|
void Stop() override;
|
||||||
|
void ReleaseResources() override;
|
||||||
|
|
||||||
|
bool IsInitialized() const override { return mInitialized.load(std::memory_order_acquire); }
|
||||||
|
bool IsRunning() const override { return mRunning.load(std::memory_order_acquire); }
|
||||||
|
VideoIOPixelFormat CapturePixelFormat() const override { return mCapturePixelFormat.load(std::memory_order_acquire); }
|
||||||
|
NdiInputMetrics Metrics() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain();
|
||||||
|
bool ConnectToConfiguredSource();
|
||||||
|
bool ConnectToDefaultSource();
|
||||||
|
void DisconnectReceiver();
|
||||||
|
void HandleVideoFrame(void* receiver, const void* frame);
|
||||||
|
bool SubmitFrame(const void* bytes, unsigned width, unsigned height, unsigned rowBytes, VideoIOPixelFormat pixelFormat, uint64_t frameIndex);
|
||||||
|
void ConfigureMailboxIfNeeded(unsigned width, unsigned height, unsigned rowBytes, VideoIOPixelFormat pixelFormat);
|
||||||
|
void LogConfiguredShapeMismatchOnce(unsigned width, unsigned height);
|
||||||
|
void LogFirstFrameOnce(unsigned width, unsigned height, VideoIOPixelFormat pixelFormat);
|
||||||
|
const char* CaptureFormatName() const;
|
||||||
|
|
||||||
|
InputFrameMailbox& mMailbox;
|
||||||
|
NdiInputConfig mConfig;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mInitialized{ false };
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<VideoIOPixelFormat> mCapturePixelFormat{ VideoIOPixelFormat::Bgra8 };
|
||||||
|
std::atomic<uint64_t> mCapturedFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mNoInputSourceFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mUnsupportedFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mSubmitMisses{ 0 };
|
||||||
|
std::atomic<double> mSubmitMilliseconds{ 0.0 };
|
||||||
|
std::atomic<bool> mLoggedFirstFrame{ false };
|
||||||
|
std::atomic<bool> mLoggedNoSource{ false };
|
||||||
|
std::atomic<bool> mLoggedUnsupportedFrame{ false };
|
||||||
|
std::atomic<bool> mLoggedSubmitMiss{ false };
|
||||||
|
std::atomic<bool> mLoggedShapeMismatch{ false };
|
||||||
|
std::mutex mReceiverMutex;
|
||||||
|
void* mReceiver = nullptr;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user