From 3bd6aeb52f355ea9a38bedc585f8d5f50ca3e9b6 Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 22 May 2026 16:25:56 +1000 Subject: [PATCH] NDI input --- CMakeLists.txt | 46 ++++ config/runtime-host.json | 4 +- src/app/VideoBackendFactory.cpp | 86 ++++++++ src/video/ndi/NdiInput.cpp | 363 ++++++++++++++++++++++++++++++++ src/video/ndi/NdiInput.h | 77 +++++++ 5 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 src/video/ndi/NdiInput.cpp create mode 100644 src/video/ndi/NdiInput.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c7dcda..82cc5bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") 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(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 "${SRC_DIR}" @@ -38,6 +39,7 @@ set(VIDEO_SHADER_INCLUDE_DIRS "${SRC_DIR}/video" "${SRC_DIR}/video/core" "${SRC_DIR}/video/decklink" + "${SRC_DIR}/video/ndi" "${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_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 "${SRC_DIR}/RenderCadenceCompositor.cpp" @@ -150,6 +157,19 @@ else() ) add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES}) 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}" + "$/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 opengl32 Ole32 @@ -216,6 +236,32 @@ else() message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}") 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" DESTINATION "." ) diff --git a/config/runtime-host.json b/config/runtime-host.json index 4df5063..019a5ac 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -5,8 +5,8 @@ "oscPort": 9000, "oscSmoothing": 0.18, "input": { - "backend": "decklink", - "device": "default", + "backend": "ndi", + "device": "AIDENLAPTOP (NVIDIA GeForce RTX 4090 Laptop GPU 1)", "resolution": "1080p", "frameRate": "59.94" }, diff --git a/src/app/VideoBackendFactory.cpp b/src/app/VideoBackendFactory.cpp index 3ccf097..98da13b 100644 --- a/src/app/VideoBackendFactory.cpp +++ b/src/app/VideoBackendFactory.cpp @@ -5,6 +5,7 @@ #include "video/decklink/DeckLinkInput.h" #include "video/decklink/DeckLinkInputThread.h" #include "video/decklink/DeckLinkOutput.h" +#include "video/ndi/NdiInput.h" #include #include @@ -199,6 +200,78 @@ private: 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 CreateDeckLinkOutputBackend() { return std::make_unique(); @@ -233,6 +306,18 @@ std::unique_ptr StartDeckLinkInputBackend(const VideoI return session; } +std::unique_ptr StartNdiInputBackend(const VideoInputBackendStartContext& context) +{ + auto session = std::make_unique(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("ndi"); + } + return session; +} + const std::vector& VideoOutputBackendRegistry() { static const std::vector registry = { @@ -246,6 +331,7 @@ const std::vector& VideoInputBackendRegistry() { static const std::vector registry = { VideoInputBackendRegistration{ { "decklink" }, true, &StartDeckLinkInputBackend }, + VideoInputBackendRegistration{ { "ndi" }, false, &StartNdiInputBackend }, VideoInputBackendRegistration{ { "none", "disabled", "off" }, false, &StartNoInputBackend }, }; return registry; diff --git a/src/video/ndi/NdiInput.cpp b/src/video/ndi/NdiInput.cpp new file mode 100644 index 0000000..ae515fd --- /dev/null +++ b/src/video/ndi/NdiInput.cpp @@ -0,0 +1,363 @@ +#include "NdiInput.h" + +#include "logging/Logger.h" +#include "video/core/VideoIOFormat.h" + +#include + +#include +#include +#include +#include + +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 lock(gNdiRuntimeMutex); + if (gNdiRuntimeUsers == 0 && !NDIlib_initialize()) + return false; + ++gNdiRuntimeUsers; + return true; +} + +void ReleaseNdiRuntime() +{ + std::lock_guard 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(fourCc); + text[0] = static_cast(value & 0xff); + text[1] = static_cast((value >> 8) & 0xff); + text[2] = static_cast((value >> 16) & 0xff); + text[3] = static_cast((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(mReceiver), + &videoFrame, + nullptr, + nullptr, + kCaptureTimeoutMilliseconds); + + if (frameType == NDIlib_frame_type_video) + { + HandleVideoFrame(mReceiver, &videoFrame); + NDIlib_recv_free_video_v2(static_cast(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 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 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 lock(mReceiverMutex); + if (mReceiver != nullptr) + { + NDIlib_recv_destroy(static_cast(mReceiver)); + mReceiver = nullptr; + } +} + +void NdiInput::HandleVideoFrame(void*, const void* frame) +{ + const NDIlib_video_frame_v2_t& videoFrame = *static_cast(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(videoFrame.xres), + static_cast(videoFrame.yres), + static_cast(videoFrame.line_stride_in_bytes), + pixelFormat, + frameIndex)) + { + mCapturedFrames.fetch_add(1, std::memory_order_relaxed); + LogConfiguredShapeMismatchOnce(static_cast(videoFrame.xres), static_cast(videoFrame.yres)); + LogFirstFrameOnce(static_cast(videoFrame.xres), static_cast(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::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)); +} +} diff --git a/src/video/ndi/NdiInput.h b/src/video/ndi/NdiInput.h new file mode 100644 index 0000000..43dce46 --- /dev/null +++ b/src/video/ndi/NdiInput.h @@ -0,0 +1,77 @@ +#pragma once + +#include "frames/InputFrameMailbox.h" +#include "video/core/VideoIOEdges.h" + +#include +#include +#include +#include +#include +#include + +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 mInitialized{ false }; + std::atomic mRunning{ false }; + std::atomic mStopping{ false }; + std::atomic mCapturePixelFormat{ VideoIOPixelFormat::Bgra8 }; + std::atomic mCapturedFrames{ 0 }; + std::atomic mNoInputSourceFrames{ 0 }; + std::atomic mUnsupportedFrames{ 0 }; + std::atomic mSubmitMisses{ 0 }; + std::atomic mSubmitMilliseconds{ 0.0 }; + std::atomic mLoggedFirstFrame{ false }; + std::atomic mLoggedNoSource{ false }; + std::atomic mLoggedUnsupportedFrame{ false }; + std::atomic mLoggedSubmitMiss{ false }; + std::atomic mLoggedShapeMismatch{ false }; + std::mutex mReceiverMutex; + void* mReceiver = nullptr; +}; +}