Files
video-shader-toys/src/video/ndi/NdiOutput.cpp
Aiden c5f0a9df0e
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m47s
CI / Windows Release Package (push) Has been skipped
NDI output flip
2026-05-22 17:04:27 +10:00

328 lines
9.9 KiB
C++

#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 <vector>
#include <utility>
namespace RenderCadenceCompositor
{
namespace
{
std::string ResolveSenderName(const std::string& configuredName)
{
if (configuredName.empty() || configuredName == "default" || configuredName == "auto")
return "Render Cadence";
return configuredName;
}
constexpr std::size_t kBgraBytesPerPixel = 4;
void SetCommonVideoFrameFields(
NDIlib_video_frame_v2_t& ndiFrame,
unsigned int width,
unsigned int height,
int rowBytes,
void* pixels,
const VideoOutputEdgeConfig& config)
{
std::memset(&ndiFrame, 0, sizeof(ndiFrame));
ndiFrame.xres = static_cast<int>(width);
ndiFrame.yres = static_cast<int>(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 = height > 0 ? static_cast<float>(width) / static_cast<float>(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*>(pixels);
ndiFrame.line_stride_in_bytes = rowBytes;
}
bool CopyBgra8FrameFlippedVertically(const VideoIOOutputFrame& frame, std::vector<unsigned char>& outputPixels, int& outputRowBytes)
{
if (frame.bytes == nullptr || frame.width == 0 || frame.height == 0 || frame.rowBytes <= 0)
return false;
const std::size_t width = static_cast<std::size_t>(frame.width);
const std::size_t height = static_cast<std::size_t>(frame.height);
const std::size_t sourceRowBytes = static_cast<std::size_t>(frame.rowBytes);
const std::size_t destinationRowBytes = width * kBgraBytesPerPixel;
if (sourceRowBytes < destinationRowBytes)
return false;
outputPixels.resize(destinationRowBytes * height);
const unsigned char* sourceBytes = static_cast<const unsigned char*>(frame.bytes);
for (std::size_t y = 0; y < height; ++y)
{
const unsigned char* sourceRow = sourceBytes + (height - 1u - y) * sourceRowBytes;
unsigned char* destinationRow = outputPixels.data() + y * destinationRowBytes;
std::memcpy(destinationRow, sourceRow, destinationRowBytes);
}
outputRowBytes = static_cast<int>(destinationRowBytes);
return true;
}
}
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 (frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8)
{
++mScheduleFailures;
return false;
}
void* completedBuffer = nullptr;
{
std::lock_guard<std::mutex> sdkLock(mSdkMutex);
void* sender = nullptr;
{
std::lock_guard<std::mutex> stateLock(mMutex);
if (!mRunning.load(std::memory_order_acquire) || mSender == nullptr)
{
++mScheduleFailures;
return false;
}
sender = mSender;
}
std::vector<unsigned char>& stagingBuffer = mStagingBuffers[mNextStagingBuffer];
int stagingRowBytes = 0;
if (!CopyBgra8FrameFlippedVertically(frame, stagingBuffer, stagingRowBytes))
{
++mScheduleFailures;
return false;
}
NDIlib_video_frame_v2_t ndiFrame;
SetCommonVideoFrameFields(ndiFrame, frame.width, frame.height, stagingRowBytes, stagingBuffer.data(), mConfig);
const auto scheduleStart = std::chrono::steady_clock::now();
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(sender), &ndiFrame);
mScheduleCallMilliseconds.store(
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(std::chrono::steady_clock::now() - scheduleStart).count(),
std::memory_order_relaxed);
mNextStagingBuffer = (mNextStagingBuffer + 1) % 2;
mHasInFlightFrame.store(true, std::memory_order_release);
{
std::lock_guard<std::mutex> stateLock(mMutex);
completedBuffer = mPendingBuffer;
mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes;
}
}
CompleteBuffer(completedBuffer, VideoIOCompletionResult::Completed);
return true;
}
void NdiOutput::Stop()
{
void* flushedBuffer = nullptr;
{
std::lock_guard<std::mutex> sdkLock(mSdkMutex);
void* sender = nullptr;
{
std::lock_guard<std::mutex> stateLock(mMutex);
sender = mSender;
}
if (sender != nullptr)
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(sender), nullptr);
std::lock_guard<std::mutex> stateLock(mMutex);
flushedBuffer = mPendingBuffer;
mPendingBuffer = nullptr;
mRunning.store(false, std::memory_order_release);
mHasInFlightFrame.store(false, std::memory_order_release);
}
CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed);
}
void NdiOutput::ReleaseResources()
{
void* sender = nullptr;
void* flushedBuffer = nullptr;
{
std::lock_guard<std::mutex> sdkLock(mSdkMutex);
{
std::lock_guard<std::mutex> stateLock(mMutex);
sender = mSender;
mSender = nullptr;
flushedBuffer = mPendingBuffer;
mPendingBuffer = nullptr;
mHasInFlightFrame.store(false, std::memory_order_release);
}
if (sender != nullptr)
{
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(sender), nullptr);
NDIlib_send_destroy(static_cast<NDIlib_send_instance_t>(sender));
}
}
CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed);
if (sender != 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::CompleteBuffer(void* buffer, VideoIOCompletionResult result)
{
if (buffer == nullptr)
return;
VideoIOCompletion completion;
completion.result = result;
completion.outputFrameBuffer = buffer;
++mCompletions;
if (result == VideoIOCompletionResult::Dropped)
++mDropped;
else if (result == VideoIOCompletionResult::Flushed)
++mFlushed;
if (mCompletionCallback)
mCompletionCallback(completion);
}
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.";
}
}